# -*- encoding: utf-8 -*-
# @Author：lijinxi
# @Time ：2020/11/29 21:55
# @File：core_ui.py
import ctypes
import inspect
import logging
import multiprocessing
import os
import sys
import threading
import time
import webbrowser
from datetime import datetime

import cv2
from PyQt6.QtWidgets import QMessageBox, QTreeWidgetItem
from PyQt6.QtCore import QRegularExpression
from PyQt6.QtGui import QRegularExpressionValidator
from PyQt6.QtCore import pyqtSignal, Qt
from PyQt6.QtGui import QFontDatabase, QFont
from PyQt6.QtGui import QIcon, QTextCursor, QImage, QPixmap
from PyQt6.QtWidgets import QWidget, QApplication, QMessageBox
from PyQt6.uic import loadUi

from qt_material import apply_stylesheet

from image_dict import *
from socket_client import SocketClient


class CoreUI(QWidget):
    log_queue = multiprocessing.Queue()  # 日志队列
    log_signal = pyqtSignal(str)  # 日志信号

    local_select_node_signal = pyqtSignal(str)
    remote_select_signal = pyqtSignal(str)

    def __init__(self):
        super(CoreUI, self).__init__()
        loadUi('ui/core.ui', self)
        self.setWindowIcon(QIcon('icons/icon.jpg'))

        self.display_image(self.wechatLabel)

        self.valid_addr_port_user_passwd()  # 校验输入

        # 远程主机信息
        self.host = None
        self.port = None
        self.user = None
        self.password = None

        self.socket_client = None  # 连接端
        self.connectButton.setIcon(QIcon('icons/error.png'))
        self.connectButton.clicked.connect(self.connect)

        # 日志信号
        self.log_signal.connect(lambda log: self.log_output(log))
        self.log_output_thread = threading.Thread(target=self.receive_log, daemon=True)
        self.log_output_thread.start()

        # 文件选择信号
        self.local_select_node_signal.connect(lambda file: self.local_show_select_file(file))
        self.local_select_thread = threading.Thread(target=self.local_node_select, daemon=True)
        self.local_select_thread.start()

        self.remote_select_signal.connect(lambda file: self.remote_show_select_file(file))
        self.remote_select_thread = threading.Thread(target=self.remote_node_select, daemon=True)
        self.remote_select_thread.start()

        self.local_path = '.'
        self.local_path = os.path.abspath(self.local_path)  # 转换为绝对路径
        self.remote_path = '.'

        self.tree_widget_initial(False)
        self.tree_widget_initial()
        self.local_file_dir_show()

        self.localBackButton.clicked.connect(self.local_back)
        self.uploadButton.clicked.connect(self.upload_file)

        # 图标初始化
        self.localBackButton.setIcon(QIcon('icons/back.png'))
        self.remoteBackButton.setIcon(QIcon('icons/back.png'))
        self.uploadButton.setIcon(QIcon('icons/upload.png'))
        self.downloadButton.setIcon(QIcon('icons/download.png'))
        self.gitButton.setIcon(QIcon('icons/git.png'))

        self.gitButton.clicked.connect(self.open_github)

        self.setFont(self.set_font(12))

    @staticmethod
    def open_github():
        url = 'https://gitee.com/yfljx/'
        t = threading.Thread(target=webbrowser.open, args=(url,))
        t.start()  # 就这样....

    @staticmethod
    def set_font(font_size):
        fid = QFontDatabase.addApplicationFont('fonts/fzqgjt.ttf')
        font = QFont()
        font.setFamily(QFontDatabase.applicationFontFamilies(fid)[0])
        font.setPointSize(font_size)
        return font

    # 文件窗口初始化
    def tree_widget_initial(self, local=True):
        if local:
            tree = self.localTreeWidget
        else:
            tree = self.remoteTreeWidget
        # 初始化TreeWidget
        tree.setColumnCount(2)
        tree.setHeaderLabels(['文件', '大小'])
        tree.setColumnWidth(0, 240)
        tree.setColumnWidth(1, 140)

    @staticmethod
    def display_image(label):
        # BGR -> RGB
        img = cv2.imread('wechat.jpg')
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        # default：The image is stored using 8-bit indexes into a colormap， for example：a gray image
        qformat = QImage.Format.Format_Indexed8

        if len(img.shape) == 3:  # rows[0], cols[1], channels[2]
            if img.shape[2] == 4:
                # The image is stored using a 32-bit byte-ordered RGBA format (8-8-8-8)
                # A: alpha channel，不透明度参数。如果一个像素的alpha通道数值为0%，那它就是完全透明的
                qformat = QImage.Format.Format_RGBA8888
            else:
                qformat = QImage.Format.Format_RGB888

        # img.shape[1]：图像宽度width，img.shape[0]：图像高度height，img.shape[2]：图像通道数
        # QImage.__init__ (self, bytes data, int width, int height, int bytesPerLine, Format format)
        # 从内存缓冲流获取img数据构造QImage类
        # img.strides[0]：每行的字节数（width*3）,rgb为3，rgba为4
        # strides[0]为最外层(即一个二维数组所占的字节长度)，strides[1]为次外层（即一维数组所占字节长度），strides[2]为最内层（即一个元素所占字节长度）
        # 从里往外看，strides[2]为1个字节长度（uint8），strides[1]为3*1个字节长度（3即rgb 3个通道）
        # strides[0]为width*3个字节长度，width代表一行有几个像素

        out_image = QImage(img, img.shape[1], img.shape[0], img.strides[0], qformat)
        label.setPixmap(QPixmap.fromImage(out_image))
        label.setScaledContents(True)  # 图片自适应大小

    def upload_file(self):
        local_path = os.path.join(self.localLineEdit.text(), self.localSelectLineEdit.text())
        remote_path = os.path.join(self.remoteLineEdit.text(), self.localSelectLineEdit.text())
        print(local_path, remote_path, 'upload')
        self.socket_client.upload(local_path, remote_path)

    def download_file(self):
        local_path = os.path.join(self.localLineEdit.text(), self.remoteSelectLineEdit.text())
        remote_path = os.path.join(self.remoteLineEdit.text(), self.remoteSelectLineEdit.text())
        print(local_path, remote_path)
        self.socket_client.download(local_path, remote_path)

    # 本地主机文件显示
    def local_file_dir_show(self):
        folders = []
        files = []
        sizes = []

        for i in os.listdir(self.local_path):
            if os.path.isdir(os.path.join(self.local_path, i)):
                folders.append(i)
            else:
                size = str(round(os.stat(os.path.join(self.local_path, i)).st_size / 1024))
                files.append(i)
                sizes.append(size)
        self.refresh_folders_files(folders, files, sizes)
        # 更改组件值
        self.localLineEdit.setText(self.local_path)

    # next按钮事件
    def local_next(self):
        self.local_path = os.path.join(self.localLineEdit.text(), self.localSelectLineEdit.text())
        self.local_file_dir_show()

    def remote_next(self):
        self.remote_path = os.path.join(self.remoteLineEdit.text(), self.remoteSelectLineEdit.text())
        self.remote_file_dir_show()
        self.remoteSelectLineEdit.setText('')

    # back按钮事件
    def local_back(self):
        self.local_path = os.path.dirname(self.localLineEdit.text())
        self.local_file_dir_show()
        self.localSelectLineEdit.setText('')

    def remote_back(self):
        self.remote_path = os.path.dirname(self.remoteLineEdit.text())
        self.remote_file_dir_show()
        self.remoteSelectLineEdit.setText('')

    # 刷新文件展示
    def refresh_folders_files(self, folders, files, sizes, local=True):
        # 选择刷新窗口
        if local:
            tree = self.localTreeWidget
        else:
            tree = self.remoteTreeWidget

        # 刷新文件夹
        # tree.clear()
        for item in tree.findItems("", Qt.MatchFlag.MatchContains):  # 很不错的一个解决方法
            item.setHidden(True)
        for f in folders:
            folder = QTreeWidgetItem(tree)
            folder.setText(0, f)
            folder.setText(1, '-')
            folder.setTextAlignment(1, Qt.TextFlag.TextWordWrap)
            folder.setIcon(0, QIcon(image_dict['folder']))

        # 刷新文件
        for f, s in zip(files, sizes):
            file = QTreeWidgetItem(tree)
            file.setText(0, f)
            file.setText(1, s + 'KB')
            file.setTextAlignment(1, Qt.TextFlag.TextWordWrap)

            file_type = f.split('.')[-1]
            if file_type in image_dict.keys():
                file.setIcon(0, QIcon(image_dict[file_type]))
            else:
                file.setIcon(0, QIcon(image_dict['unknown']))

    # 本地主机文件选中
    def local_show_select_file(self, file):
        self.localSelectLineEdit.setText(file)
        file_path = os.path.join(self.local_path, file)
        if os.path.isdir(file_path):
            self.local_next()  # 目录则跳转
            self.localSelectLineEdit.setText('')

    def local_node_select(self):
        prev_file = self.localSelectLineEdit.text()
        while True:
            item = self.localTreeWidget.currentItem()
            if item:
                text = item.text(0)
                if prev_file != text:  # 避免重复循环
                    self.local_select_node_signal.emit(text)
                    prev_file = item.text(0)
                    time.sleep(1)
            else:
                continue

    # 远程主机文件选中
    def remote_show_select_file(self, file):
        self.remoteSelectLineEdit.setText(file)
        file_path = os.path.join(self.remote_path, file)
        if os.path.isdir(file_path):
            self.remote_next()  # 目录则跳转
            self.remoteSelectLineEdit.setText('')  # 跳转后为空

    def remote_node_select(self):
        prev_file = self.remoteSelectLineEdit.text()
        while True:
            item = self.remoteTreeWidget.currentItem()
            if item:
                if prev_file != item.text(0):  # 避免重复循环
                    self.remote_select_signal.emit(item.text(0))  # 发送信号
                    prev_file = item.text(0)
                    time.sleep(1)
            else:
                continue

    # 校验控件输入
    def valid_addr_port_user_passwd(self):
        # host
        host_regx = QRegularExpression(r'^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$')
        host_validator = QRegularExpressionValidator(host_regx, self.hostLineEdit)
        self.hostLineEdit.setValidator(host_validator)
        # user
        user_regx = QRegularExpression(r'^[a-z|A-Z|0-9]*$')
        user_validator = QRegularExpressionValidator(user_regx, self.userLineEdit)
        self.userLineEdit.setValidator(user_validator)
        # password
        password_regx = QRegularExpression(r'^[a-z|A-Z|0-9]*$')
        password_validator = QRegularExpressionValidator(password_regx, self.passwordLineEdit)
        self.passwordLineEdit.setValidator(password_validator)

    # 获取登陆信息
    def get_login_info(self):
        self.host = self.hostLineEdit.text().strip()
        self.port = int(self.portLineEdit.text().strip())
        self.user = self.userLineEdit.text().strip()
        self.password = self.passwordLineEdit.text().strip()

    # 连接远程主机
    def connect(self):
        self.get_login_info()
        self.socket_client = SocketClient(self.log_queue, host=self.host, port=self.port, username=self.user,
                                          password=self.password)
        status_code = self.socket_client.connect_valid()
        print(status_code)  # 状态码
        if status_code == 'OK':
            self.connectButton.setIcon(QIcon('icons/success.png'))
            self.connectButton.setText('连接成功')
            self.log_queue.put('已经连接到远程主机, {}:{}.'.format(self.host, self.port))
            self.log_queue.put('当前用户: {} .'.format(self.user))
            self.remote_file_dir_show()
            # 添加远程主机信号
            # 需要修改
            self.remoteBackButton.clicked.connect(self.remote_back)

            self.downloadButton.clicked.connect(self.download_file)

        else:
            self.connectButton.setIcon(QIcon('icons/error.png'))  # 连接失败
            self.connectButton.setText('连接失败')

    # 远程主机文件显示
    def remote_file_dir_show(self):
        abs_path, folders, files, sizes = self.socket_client.ls_remote_dir(self.remote_path)
        self.refresh_folders_files(folders, files, sizes, False)
        self.remote_path = abs_path
        # 更改组件值
        self.remoteLineEdit.setText(self.remote_path)

    def receive_log(self):
        while True:
            data = self.log_queue.get()
            if data:
                self.log_signal.emit(data)  # 发送日志信息
            else:
                continue

    # 日志输出
    def log_output(self, log):
        _time = datetime.now().strftime('[%Y/%m/%d %H:%M:%S]')
        log = _time + ' ' + log + '\n'
        logging.info(log)
        self.logTextEdit.moveCursor(QTextCursor.End)
        self.logTextEdit.insertPlainText(log)
        self.logTextEdit.ensureCursorVisible()  # 自动滚屏

    @staticmethod
    def _async_raise(tid, exec_type):  # 结束线程的好方法
        """raises the exception, performs cleanup if needed"""
        tid = ctypes.c_long(tid)
        if not inspect.isclass(exec_type):
            exec_type = type(exec_type)
        res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exec_type))
        if res == 0:
            raise ValueError("invalid thread id")
        elif res != 1:
            # """if it returns a number greater than one, you're in trouble,
            # and you should call it again with exc=NULL to revert the effect"""
            ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
            raise SystemError("PyThreadState_SetAsyncExc failed")

    def stop_thread(self, thread):
        self._async_raise(thread.ident, SystemExit)

    def closeEvent(self, event) -> None:
        text = '<font color=green ><center><b>关闭此应用</b></center></font> '
        informative_text = '<font color=red ><b><center>是否关闭</center></b></font>'
        ret = self.call_dialog(QMessageBox.Question, text, informative_text, QMessageBox.Yes | QMessageBox.No,
                               QMessageBox.No)
        if ret == QMessageBox.Yes:
            self.stop_thread(self.local_select_thread)
            self.stop_thread(self.remote_select_thread)
            self.stop_thread(self.log_output_thread)
            event.accept()
        else:
            event.ignore()

    @staticmethod
    def call_dialog(icon, text, informative_text, standard_buttons, default_button=None):
        msg = QMessageBox()
        msg.setWindowIcon(QIcon('icons/icon.jpg'))
        msg.setWindowTitle('文件传输管理器')
        # msg.setFont('方正启体简体')
        msg.setIcon(icon)
        msg.setText(text)
        msg.setFont(CoreUI.set_font(10))
        msg.setInformativeText(informative_text)
        msg.setStandardButtons(standard_buttons)
        if default_button:
            msg.setDefaultButton(default_button)
        return msg.exec_()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    widget = CoreUI()
    # apply_stylesheet(app, theme='dark_teal.xml')
    widget.show()
    sys.exit(app.exec())
